iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0
Build on Google AI

30 天打造 AI Web Security Agent:從 Google AI Studio 到 Gemini Agent系列 第 18 篇

Day 18|留言先存起來,下一次開頁面才執行:我怎麼確認儲存型 XSS?

  • 分享至 

  • xImage
  •  

前言

前幾天,我追蹤的是輸入隨當次請求進入回應,或從網址片段進入瀏覽器 DOM。今天換成留言功能:表單先送出文字,伺服器保存,再由後續的讀取請求產生頁面。這段時間差是儲存型 XSS 的重點。MDN 用留言作為例子說明:網站接受並保存內容,之後又把內容放進提供給讀者的頁面,若沒有妥善處理,就可能形成儲存型 XSS

我要分開回答 4 件事:表單送到哪裡、POST 是否真的保存、之後 GET 回傳了什麼、Chrome 是否執行本次探針。只看到 201 或回應裡有 alert(1) 文字,都還不能回答最後一題。

三種處理方式,共用一個留言欄位

我在 127.0.0.1 建立 3 組本機表單。每個表單都使用 method="post",欄位名稱是 message,但提交與顯示方式不同:

組別 提交後的處理 讀取頁的處理
原樣插入 保存留言 直接放入 HTML 的 <div>
HTML 跳脫 保存留言 將留言中的特殊字元轉為 HTML 實體
拒絕提交 回傳 422,不保存 顯示固定的「沒有已儲存內容」

本次只把資料存在伺服器記憶體中,並在每組開始前清空。這足以檢查「POST 之後,另一個 GET 再讀到」的流程;它沒有模擬資料庫、不同帳號或長期保存。

我保留每組表單的完整原始 HTML,也另外解析 action、method 和欄位名稱。如此一來,後續整理表單時,可以從原始檔重新核對,而不只相信程式寫出的摘要。

用固定探針核對「真的執行」

我為每組產生獨立識別碼,將下面形狀的探針作為留言送出:

<img src="/day18-missing.png" alt="" onerror="document.documentElement.setAttribute('data-day18-xss','本次識別碼');alert(1)">

圖片只指向本機刻意不存在的檔案。若瀏覽器真的把留言解析成 <img>,載入失敗會觸發 onerror。我要求 Chrome 同時捕捉到訊息為 1 的對話框,並在 <html> 讀回與本次識別碼相符的 data-day18-xss,才把固定探針記為已執行。探針沒有讀取 Cookie,也沒有把資料送往外部。

MDN 的 XSS 範例同樣說明,圖片載入錯誤事件可以讓插入的 HTML 執行程式;所以我需要檢查 DOM 真的形成元素,而不能只在回應文字裡搜尋 <img。

先送 POST,再用另一個 GET 讀取

我從專案根目錄執行:

.venv/bin/python day-18/run_experiment.py

程式先取得每組的表單原始 HTML,再以 message 提交固定探針。POST 的原始請求與回應分開保存;完成提交後,程式用另一個 GET 取得讀取頁的原始 HTML,最後才讓 Chrome 開啟該讀取頁。三組的表單都是 POST /{組別}/submit,後續讀取都是 GET /{組別}/view。伺服器的請求紀錄也保留了這個先後順序。

弱點組與跳脫組的 POST 都回傳 201,而且伺服器記憶體都保存了完整探針;拒絕組回傳 422,伺服器沒有保存它。可見「保存成功」只回答了資料是否留在伺服器,不等於執行。

同一份留言,讀取頁出現三種結果

弱點組的後續 GET 把探針原樣放進 HTML。Chrome 在 #result 中建立了真正的 <img>,並請求不存在的 /day18-missing.png。本次收到 alert(1),DOM 旗標是 day18-2070ed412ac1522e,與送出的識別碼相同。依雙重條件,這個本機讀取頁上的固定探針已執行,分類為 confirmed_stored_xss。

跳脫組也保存了完整留言,但後續 GET 將開頭的 <img 寫成 &lt;img。Chrome 的 textContent 仍能讀回完整探針字面文字,DOM 裡卻沒有圖片元素,也沒有對話框或相符旗標。本次分類為 stored_as_text。

拒絕組的 POST 直接回傳 422,後續讀取頁只顯示「沒有已儲存內容」。這次探針沒有被保存,也沒有在讀取頁出現,分類為 submission_rejected。這是本組特意設計的對照;一般留言功能不能靠一律拒絕輸入來代替合適的顯示處理。

組別 POST 後續 GET Chrome 雙重訊號 本次分類
原樣插入 201,已保存 原始 <img> alert(1) 與相符旗標 confirmed_stored_xss
HTML 跳脫 201,已保存 &lt;img,只作文字 都沒有 stored_as_text
拒絕提交 422,未保存 固定文字 都沒有 submission_rejected

我還執行了 4 個測試,檢查提交與讀取確實分開、原始檔的 SHA-256 一致、兩種未執行情境沒有被誤判,以及偵測器拒絕遠端目標。完整表單、POST、GET、Chrome 結果都保存在 day-18/results/。

Day 18:儲存只是資料流的中間站

這次弱點組的證據不是單一的「回應包含探針」。同一段輸入先進入表單 POST、留在伺服器記憶體,再由後續 GET 進入 HTML,最後在 Chrome 形成圖片元素並出現雙重執行訊號。跳脫組停在「保存後作文字顯示」;拒絕組則在提交階段就停止。

這些結論只適用於本機、單一 message 欄位及固定 img onerror 探針。我沒有測試持久資料庫、跨帳號瀏覽、其他輸出位置、CSP 或正式網站;也不能因為跳脫組與拒絕組沒有觸發,就推論整個網站安全。

那就…
明天見!


上一篇
Day 17|把 POST 證據交給 AI,報告能分清「送達」與「執行」嗎?
下一篇
Day 19|把留言的跨請求路徑交給 AI,它能分清「已儲存」與「已執行」嗎?
系列文
30 天打造 AI Web Security Agent:從 Google AI Studio 到 Gemini Agent 共 20 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言